/*
 * Copyright 2011 Christoph Ludwig (christoph[at]chludwig.de). All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.chludwig.ehumanities.provenance.persistence;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.transform.stream.StreamSource;

import de.chludwig.ehumanities.provenance.entities.LogEntry;
import de.chludwig.ehumanities.provenance.exception.ProvenanceFileNotFoundException;
import de.chludwig.ehumanities.provenance.exception.ProvenanceIOException;
import de.chludwig.ehumanities.provenance.exception.ProvenanceJAXBException;
import de.chludwig.ehumanities.provenance.jaxb.LogEntryMetadataType;
import de.chludwig.ehumanities.provenance.jaxb.MarshallerFactory;
import de.chludwig.ehumanities.provenance.jaxb.ObjectFactory;
import de.chludwig.ehumanities.provenance.jaxb.UnmarshallUtil;
import de.chludwig.ehumanities.provenance.utils.StreamUtil;

/**
 * 
 */
public class LogEntryPO {

  private WorkflowContainerPO containerPO;
  private UUID entryId;
  private File entryFolder;
  private static final String DATA_FILE_NAME = "data";
  private static final String METADATA_FILE_NAME = "metadata";

  public LogEntryPO(UUID containerId, UUID entryId) {
    StorageArea storageArea = new StorageArea();
    this.containerPO = new WorkflowContainerPO(storageArea, containerId);
    this.entryId = entryId;
    this.entryFolder = null;
  }
  
  public LogEntryPO(WorkflowContainerPO containerPO, UUID entryId) {
    this.containerPO = containerPO;
    this.entryId = entryId;
    this.entryFolder = null;
  }

  /**
   * @return the containerPO
   */
  public WorkflowContainerPO getContainerPO() {
    return containerPO;
  }

  /**
   * @return the entryId
   */
  public UUID getEntryId() {
    return entryId;
  }

  /**
   * @return the entryFolder
   */
  public File getEntryFolder() {
    if (entryFolder == null) {
      File entriesFolder = containerPO.getEntriesFolder();
      entryFolder = new File(entriesFolder, entryId.toString());
    }
    return entryFolder;
  }

  /**
   * @param entryFolder
   * @return
   */
  public File getMetadataFile() {
    return new File(getEntryFolder(), METADATA_FILE_NAME);
  }

  /**
   * @param entryFolder
   * @return
   */
  public File getDataFile() {
    return new File(getEntryFolder(), DATA_FILE_NAME);
  }

  public LogEntryMetadataType readMetadata() {
    try {
      File metadataFile = getReadableMetadataFile();
      StreamSource source = new StreamSource(metadataFile);
      JAXBElement<LogEntryMetadataType> elem = UnmarshallUtil.logEntryMetadataElement(source);
      return elem.getValue();
    }
    catch (JAXBException ex) {
      throw new ProvenanceJAXBException("cannot read log entry metadata", ex);
    }
  }

  private File getReadableMetadataFile() {
    File metadataFile = getMetadataFile();
    if (!(metadataFile.isFile() && metadataFile.canRead())) {
      String msg = "metadata file " + metadataFile + " does not exist or is not a readable regular file";
      throw new ProvenanceFileNotFoundException(msg, null).checkIfArchived(containerPO.getContainerId());
    }    
    return metadataFile;
  }


  public static boolean isLogEntryFolder(WorkflowContainerPO containerPO, File candidate) {
    try {
      UUID entryId = UUID.fromString(candidate.getName());
      LogEntryPO entryPO = new LogEntryPO(containerPO, entryId);
      File metadataCandidate = entryPO.getMetadataFile();
      File dataCandidate = entryPO.getDataFile();

      return metadataCandidate.isFile() && metadataCandidate.canRead() && dataCandidate.isFile()
              && dataCandidate.canRead();
    }
    catch (IllegalArgumentException ex) {
      return false;
    }
  }

  public void initialize(LogEntry entry) {
    persist(entry, false);
  }
  
  public void persistAndOverwrite(LogEntry entry) {
    persist(entry, true);
  }

  public InputStream getDataStream() {
    File source = getDataFile();
    try {
      InputStream filestream = new FileInputStream(source);
      return new BufferedInputStream(filestream);
    }
    catch (FileNotFoundException ex) {
      throw new ProvenanceIOException("cannot read data file " + source.getAbsolutePath(), ex);
    }
  }

  public void updateData(LogEntry entry, InputStream dataStream) {
    try {
      writeMetadataFile(entry.getMetadata());
      writeDataStream(dataStream);
    }
    catch (IOException ioexception) {
      throw new ProvenanceIOException("cannot update log entry", ioexception);
    }
    catch (JAXBException jaxbexception) {
      throw new ProvenanceIOException("cannot update log entry", jaxbexception);
    }
  }

  private void persist(LogEntry entry, boolean overwrite) {
    getEntryFolder().mkdirs();
    try {
      writeEmptyDataFile(overwrite);
      writeMetadataFile(entry.getMetadata());
    }
    catch (IOException ioexception) {
      throw new ProvenanceIOException("cannot create log entry", ioexception);
    }
    catch (JAXBException jaxbexception) {
      throw new ProvenanceIOException("cannot create log entry", jaxbexception);
    }
  }

  private void writeEmptyDataFile(boolean overwrite) throws IOException {
    File datafile = getDataFile();
    if (!(datafile.createNewFile() || overwrite)) {
      throw new IOException("data file " + datafile.getAbsolutePath() + " already exists");
    }
  }

  private void writeMetadataFile(LogEntryMetadataType metadata) throws JAXBException {
    Marshaller marshaller = MarshallerFactory.createMarshaller(LogEntryMetadataType.class);
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    File metadataFile = getMetadataFile();
    JAXBElement<LogEntryMetadataType> root = jaxbMetadataRootElement(metadata);
    marshaller.marshal(root, metadataFile);
  }

  private void writeDataStream(InputStream dataStream) throws IOException {
    File datafile = new File(getEntryFolder(), LogEntryPO.DATA_FILE_NAME);
    OutputStream target = new BufferedOutputStream(new FileOutputStream(datafile));
    try {
      InputStream source = new BufferedInputStream(dataStream);
      try {
        new StreamUtil().transfer(source, target);
      }
      finally {
        source.close();
      }
    }
    finally {
      target.close();
    }
  }
  
  /**
   * @param container
   * @return
   */
  private JAXBElement<LogEntryMetadataType> jaxbMetadataRootElement(LogEntryMetadataType metadata) {
    JAXBElement<LogEntryMetadataType> root = new ObjectFactory().createLogEntryMetadata(metadata);
    return root;
  }


}
